/* CS32 FINAL PROJECT */
package GameEngine;

import Objects.*;
import GameEngine.GameConstants.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 * This is the data structure which will represent a level as a whole.
 * @author mhfowler
 */
public class Level
{

    /* list of changed objects, what will be sent to the client */
    List<BasicObject> changedObjects;

    /* the status of the game */
    GameState status;

    /* the parent object which interacts with users and created/loaded this level */
    GameServer parent;

    /* how many iterations the level has existed for */
    int time;

    /* list of all interactable objects in level  */
    HashMap<Short, GameObject> objects;

    /* list of objects that will be destroyed at the end of iteration, to prevent
     * concurrent modification error */
    List<GameObject> toDestroy;
    List<GameObject> toCreate;

    // level stats
    private int width,  height,  numPlayers;

    private SpawnPoint spawnPoint;
    private boolean victory = false;

    private short tempId = -1;

    /**
     * Creates the level
     * @param levelObjects
     * @param w
     * @param h
     * @param nP
     */
    public Level(List<GameObject> levelObjects, int w, int h, int nP, SpawnPoint sp)
    {
        width = w;
        height = h;
        numPlayers = nP;
        spawnPoint = sp;
        objects = new HashMap<Short, GameObject>();
        toCreate = new LinkedList<GameObject>();
        toDestroy = new LinkedList<GameObject>();
        for (GameObject obj : levelObjects)
        {
            createObject(obj);
        }
        createObjects();
    }


    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    /**
     * Declares a victory
     */

    public void setVicFlag()
    {
        victory = true;
    }

    public HashMap<Short, GameObject> getObject()
    {
        return objects;
    }

    public int getNumPlayers() {
        return numPlayers;
    }

    public SpawnPoint getSpawnPoint() {
        return spawnPoint;
    }

    /* this is so objects can create new objects, and call their parent */
    public short createObject(GameObject toC)
    {
        short UID = getUniqueID();
        toC.setId(UID);
        toCreate.add(toC);
        return UID;
    }

    /* this is so objects can destroy objects */
    public void destroyObject(GameObject o) {
        o.setExists(false);
        toDestroy.add(o);
    }

    /* this is so objects can acquire a unique id */
    public short getUniqueID()
    {
        tempId++;
        return tempId;
    }


    /**
     * check for collisions between inputted object and any other object
     * @param obj
     * @param keyArray
     * @param i
     */
    public void checkCollisions(GameObject obj, Short[] keyArray, int i) {
        /* iterate through all other objects and check for collision
         * To avoid repeating combinations and cut comparison time in half,
         * we only compare with objects whose keys are later in the set than
         * the current object
         */

        double objDiag = Math.sqrt(obj.getSize().getX()*obj.getSize().getX() + obj.getSize().getY()*obj.getSize().getY());

        for (int j = i + 1; j < keyArray.length ; j++) {
            GameObject o = objects.get(keyArray[j]);

            /**
             * We'll eliminate a lot of the checks simply by eliminating cases
             * where two objects are too far away to be together
             */

            // if the sum of the lengths of the two objects' diagonals
            // (the longest line segment contained in their hitbox)
            // is less than the distance between them
            // they can't possibly collide.
            // so only check cases where that sum is greater than the distance between them

            // equivalently, check to see if half that sum is greater than the distance between their centers

            float diffX = obj.getCenter().getX()-o.getCenter().getX();
            float diffY = obj.getCenter().getY()-o.getCenter().getY();
            double distance = Math.sqrt(diffX*diffX + diffY*diffY);

            double objDiag2 = Math.sqrt(o.getSize().getX() * o.getSize().getX() + o.getSize().getY() * o.getSize().getY());

            // if sufficiently close
            if (distance <= (objDiag + objDiag2)/2) {
                // check if they collide
                Vector offset = checkCollision(o, obj);
                if (offset != null) {

                    float totalNorm = obj.getVelocity().getNorm() + o.getVelocity().getNorm();
                    
                    if (o.isTangible() && obj.isTangible() && !doesntCollide(o, obj)) {
                        o.offset(offset.scaleNew(-1), totalNorm);
                        obj.offset(offset, totalNorm);
                    }

                    o.collide(obj);
                    obj.collide(o);
                }
            }
        }

        
    }
    
    /**
     * Returns whether two objects of given types should collide with each other
     * @param o1
     * @param o2
     * @return
     */

    public static boolean doesntCollide(GameObject o1, GameObject o2) {
        boolean toReturn = false;
        if (o1 instanceof Player && o2 instanceof Player) {
            toReturn = true;
        }
        if (o1 instanceof Water && o2 instanceof Water) {
            toReturn = true;
        }
        if (o1 instanceof Water && o2 instanceof Player || o1 instanceof Player && o2 instanceof Water) {
            toReturn = true;
        }
        return toReturn;
    }

    /* this method will move all objects based on their delta position */
    /* it breaks each movement path down into a constant number of steps and checks
     * for collisions between each step */
    public void moveObjects() {
        Set<Short> keys = objects.keySet();
        // create array of keys from set 
        Short[] keyArray = new Short[keys.size()];
        Iterator<Short> iter = keys.iterator();
        for (int i=0; i<keys.size(); i++) {
            keyArray[i] = iter.next();
        }
        // now move the objects
        int stepsLeft = GameConstants.NUM_MOVE_STEPS;
        while (stepsLeft != 0) {
            for (int i = 0; i < keyArray.length; i++) {
                GameObject o = objects.get(keyArray[i]);
                o.moveStep(stepsLeft);
            }
            for (int i = 0; i < keyArray.length; i++) {
                GameObject o = objects.get(keyArray[i]);
                checkCollisions(o, keyArray, i);
            }
            stepsLeft--;
        }
    }


    /* this method will iterate through all objects and cause them to update their
     * attributes by calling next iteration, and then move() */
    public void update() {
        Set<Short> keys = objects.keySet();
        Iterator<Short> iter = keys.iterator();
        GameObject o;
        while (iter.hasNext()) {
            o = objects.get(iter.next());
            o.nextIteration();
            o.increaseAge();
        }
    }

    /* this is the method by which the server acquires the new game state to send to the clients,
     * the server passes the level a list of all user actions it received from clients, the level then
     * parses these actions (calling userAction) and then calls update() */
    /* finds the object with that ID and causes it to perform that action. Only Player objects
     * have a method useSpecial which takes in an action, but this ID should always be a player
     * object so it should be an okay cast */
    public List<BasicObject> updateState(List<UserAction> userActions) {
        // check if game is over
        if (victory) {
            System.out.println("VICTORYYYYYYYYYYYY");
            BasicObject toReturn = new BasicObject();
            toReturn.setId((short)-7);
            LinkedList<BasicObject> victory = new LinkedList<BasicObject>();
            victory.add(toReturn);
            return victory;
        }
        else {
        // iterate through list of user actions
        for (UserAction u : userActions) {
            Player player = (Player) objects.get(u.id);
            // System.out.println("User ID: " + player.getId() + " " + "Action: " + u.act.toString());
            player.receiveAction(u.act);
        }
        // call nextIteration on every object
        update();
        moveObjects();
        time++;
        // update list of changed objects and get to return
        List<BasicObject> toReturn = updateChanged();
        createObjects();
        destroyObjects(); // removes objects from gamestate which were destroyed this iteration
        return toReturn;
        }
    }

    
    /* returns a list of all objects in the level which collide with a square with inputted center and
     * size */
    public List<GameObject> within(Vector center, Vector vector) {
        GameObject temp = new Meta(center, vector);
        List<GameObject> toReturn = new LinkedList<GameObject>();
        // iterate through all other objects
        Set<Short> keys = objects.keySet();
        GameObject o;
        for (Short id : keys) {
            o = objects.get(id);
            if (checkCollision(o, temp) != null) {
                toReturn.add(o);
            }
        }
        return toReturn;
    }


    /**
     * Checks if two objects are colliding
     * Returns null if not, returns a Vector of displacement if so
     * @param obj1
     * @param obj2
     * @return
     */

    public static Vector checkCollision(GameObject obj1, GameObject obj2) {
        // we use the projection algorithm to see if the two objects overlap on axes parallel to their edges
        // similar to this http://www.codeproject.com/KB/GDI-plus/PolygonCollision.aspx

        // it's a known theorem that if two polygons fail to overlap on any one such axis, they must not overlap
        // so we can stop checking immediately after one axis fails
        Vector dir1 = obj1.getDir();
        Vector dir2 = obj2.getDir();
        Vector orthog1 = dir1.getOrthog();
        Vector orthog2 = dir2.getOrthog();
        // since all our objects are rectangular
        // we only need to check two axis per object
        // because the other two sides would be parallel to the first two
        Vector[] axes = {dir1, orthog1, dir2, orthog2};
        float minOverlap = Float.MAX_VALUE; // keep track of minimum overlap
        int minIndex = 0; // keep track of minimum index

        for (int i = 0; i < 4; i++) {
            Vector axis = axes[i];
            // iterate through each axis
            // project the corners onto them
            // find max and min
            Vector[] corners1 = obj1.getCorners();
            float min1 = Float.MAX_VALUE;
            float max1 = -Float.MAX_VALUE;
            for (Vector corner : corners1) {
                float scalar = corner.projectScalar(axis);
                if (scalar < min1) {
                    min1 = scalar;
                }
                if (scalar > max1) {
                    max1 = scalar;
                }
            }

            Vector[] corners2 = obj2.getCorners();
            float min2 = Float.MAX_VALUE;
            float max2 = -Float.MAX_VALUE;
            for (Vector corner : corners2) {
                float scalar = corner.projectScalar(axis);
                if (scalar < min2) {
                    min2 = scalar;
                }
                if (scalar > max2) {
                    max2 = scalar;
                }
            }

            // we can continue if max1 or min1 is between max2 and min2
            // (equivalently, if max2 or min2 is between max1 and min1)

            float overScale;

            if (min1 >= min2 && min1 <= max2) {
                // good case
                if (max1 <= max2) {
                    overScale = max1 - min1;
                } else {
                    overScale = max2 - min1;
                }
            } else if (max1 >= min2 && max1 <= max2) {
                // good case
                overScale = max1 - min2;
            } else if (min2 >= min1 && min2 <= max1) {
                // good case
                if (max2 <= max1) {
                    overScale = max2 - min2;
                } else {
                    overScale = max1 - min2;
                }
            } else {
                // failure on one axis
                return null;
            }
            // keep track of max
            if (overScale < minOverlap) {
                minOverlap = overScale;
                minIndex = i;
            }
        }

        // System.out.println("Axis of " + axes[minIndex].x + ", " + axes[minIndex].y + " -> " + minIndex);

        Vector toReturn = axes[minIndex].scaleNew(minOverlap);
        
        // need to reverse for certain cases
        // reverse if the first object's axis faces away from the other object
        // or if the second object's axis faces towards the other object
        if (minIndex < 2) {
            if (obj2.getCenter().addNew(obj1.getCenter().scaleNew(-1)).projectScalar(axes[minIndex]) <= 0) {
                toReturn.scale(-1);
            }
        } else {
            if (obj1.getCenter().addNew(obj2.getCenter().scaleNew(-1)).projectScalar(axes[minIndex]) >= 0) {
                toReturn.scale(-1);
            }
        }

        return toReturn;
    }

    /* creates all objects to be created, thus avoid concurenccy */
    public void createObjects() {
        for (GameObject o : toCreate) {
            objects.put(o.getId(), o);
            o.setLev(this);
        }
        toCreate.clear();
    }


    /* iterates through list of objects to be destroyed this iteration and destroys them */
    private void destroyObjects() {
        Iterator<GameObject> iter = toDestroy.iterator();
        GameObject o;
        while (iter.hasNext()) {
            o = iter.next();
            objects.remove(o.getId());
        }
        toDestroy.clear();
    }


    /**
     * Adds an object to the list of objects to destroy
     * @param obj
     */
    /* iterates through all game objects and returns a list of all objects that
     * were changed this iteration, cast as basic objects */
    /* note this method also 'gets rid' of objects which were destroyed this iteration,
     * but their corresponding basic objects are still sent to the client that way
     * the client's graphcis knows to remove this object */
    private List<BasicObject> updateChanged() {
        List<BasicObject> toReturn = new LinkedList<BasicObject>();
        Set<Short> keys = objects.keySet();
        BasicObject toAdd;
        GameObject o;
        for (short id : keys) {
            o = objects.get(id);
            if (o.isChanged()) {
                toAdd = (BasicObject) o;
                toReturn.add(toAdd);
                o.setChanged(false);
                // check if destroyed
                if (!o.isExists()) {
                    toDestroy.add(o);
                }
            }
        }
        return toReturn;
    }


    /* casts all objects as basics, for sending every object instead of just changed */
    public List<BasicObject> getBasics() {
        List<BasicObject> toReturn = new LinkedList<BasicObject>();
        Set<Short> keys = objects.keySet();
        BasicObject toAdd;
        GameObject o;
        for (short id : keys) {
            o = objects.get(id);
            toAdd = (BasicObject) o;
            toReturn.add(toAdd);
        }
        return toReturn;
    }

    int getnumPlayers() {
        return numPlayers;
    }




    /* takes in a player object, and changes their position to that of the spawn point */
    public void respawn(Player p) {
        System.out.println("respawned: " + spawnPoint.getCenter().toString());
        p.setCenter(new Vector(spawnPoint.getCenter()));
        p.setVelocity(new Vector(0,0));
        p.setAcceleration(new Vector(0, GameConstants.GRAVITY_ACCELERATION));
    }


    public HashMap<Short,GameObject> getHash() {
        return objects;
    }


}

